﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Threading;
using Xceed.Silverlight.Compatibility;
using Xceed.Silverlight.Utils;

namespace Xceed.Silverlight.DragDrop
{
  public sealed class DragDropManager
  {
    private DragDropManager()
    {
      ContentPresenter contentPresenter = new ContentPresenter();
      contentPresenter.SetBinding( ContentPresenter.ContentTemplateProperty, new Binding() );

      m_customCursorPopup.Child = contentPresenter;

#if !SILVERLIGHT
      m_customCursorPopup.AllowsTransparency = true;
#endif

      string cursorXaml = "<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' ><Canvas Width='16' Height='16'><Path Data='F1 M 8.000,12.950 C 7.063,12.950 6.195,12.675 5.447,12.221 L 12.222,5.447 C 12.675,6.195 12.950,7.062 12.950,8.000 C 12.950,10.733 10.733,12.950 8.000,12.950 Z M 3.050,8.000 C 3.050,5.266 5.267,3.050 8.000,3.050 C 8.839,3.050 9.617,3.278 10.310,3.646 L 3.646,10.310 C 3.278,9.617 3.050,8.839 3.050,8.000 Z M 8.000,0.000 C 3.582,0.000 0.000,3.582 0.000,8.000 C 0.000,9.687 0.525,11.250 1.417,12.540 L 1.415,12.541 L 1.424,12.550 C 2.868,14.633 5.273,16.000 8.000,16.000 C 12.418,16.000 16.000,12.418 16.000,8.000 C 16.000,3.582 12.418,0.000 8.000,0.000 Z'><Path.Fill><LinearGradientBrush MappingMode='Absolute' StartPoint='0,8' EndPoint='16,8'><GradientStop Offset='0' Color='red'/><GradientStop Offset='1' Color='#FF850000'/></LinearGradientBrush></Path.Fill></Path></Canvas></DataTemplate>";

      m_noDropCursorTemplate = CompatibilityUtils.XamlParse( cursorXaml ) as DataTemplate;
    }

    #region CurrentDragContext

    public static DragDropContext CurrentDragDropContext
    {
      get
      {
        DragDropManager manager = DragDropManager.SingletonInstance;
        if( manager != null && manager.m_isDragActive )
          return manager.m_dragContext;

        return null;
      }
    }

    private DragDropContext m_dragContext;

    #endregion

    #region Instance Static Property

    internal static DragDropManager Instance
    {
      get
      {
        if( DragDropManager.SingletonInstance == null )
        {
          DragDropManager.SingletonInstance = new DragDropManager();
        }

        return DragDropManager.SingletonInstance;
      }
    }

    private static DragDropManager SingletonInstance;

    #endregion

    #region DraggedOverReference Property

    internal static IDropTarget DraggedOverReference
    {
      get
      {
        if( ( DragDropManager.Instance.m_draggedOverReference != null ) && ( DragDropManager.Instance.m_draggedOverReference.IsAlive ) )
        {
          return DragDropManager.Instance.m_draggedOverReference.Target as IDropTarget;
        }

        return null;
      }
    }
    private WeakReference m_draggedOverReference; // null

    #endregion

    #region IsDragActive Property

    internal static bool IsDragActive
    {
      get
      {
        return DragDropManager.Instance.m_isDragActive;
      }
    }
    private bool m_isDragActive; // false

    #endregion

    #region IsDraggedElementConfigurationPreserved Property

    public static bool IsDraggedElementConfigurationPreserved 
    {
      get 
      {
        return DragDropAdornerManager.IsDraggedElementConfigurationPreserved;
      }
      set 
      {
        DragDropAdornerManager.IsDraggedElementConfigurationPreserved = value;
      }
    }

    #endregion

    public static void PrepareDrag( DragDropContext dragContext, MouseButtonEventArgs e )
    {
      DragDropManager.Instance.PrepareDragWorker( dragContext, e );
    }

    public static void CancelDrag()
    {
      DragDropManager.Instance.CancelDragCore();
    }

    public static void PauseDrag()
    {
      DragDropManager.Instance.PauseDragCore();
    }

    public static void ResumeDrag()
    {
      DragDropManager.Instance.ResumeDragCore();
    }

    private void PrepareDragWorker( DragDropContext dragContext, MouseButtonEventArgs e )
    {
      if( dragContext == null )
        throw new ArgumentNullException( "dragContext" );

      m_dragContext = dragContext;

      if( m_isDragActive )
        throw new InvalidOperationException( "An attempt was made to drag an element while one is already being dragged." );

      UIElement draggedElement = m_dragContext.DraggedElement;

      if( draggedElement == null )
        throw new ArgumentException( "The dragContext's DraggedElement cannot be null." );

      if( e == null )
        throw new ArgumentNullException( "e" );


      draggedElement.MouseMove += this.OnDraggedElementMouseMove;
      draggedElement.MouseLeftButtonUp += this.OnDraggedElementMouseLeftButtonUp;
      draggedElement.MouseEnter += this.OnDraggedElementMouseEnter;
      draggedElement.MouseLeave += this.OnDraggedElementMouseLeave;
      draggedElement.LostMouseCapture += this.OnDraggedElementLostMouseCapture;

      m_mouseLeftButtonDownLocation = e.GetPosition( null );
      dragContext.MouseOffset = e.GetPosition( draggedElement );
      m_lastMousePosition = m_mouseLeftButtonDownLocation;
    }

    private void InvokeDragEnter( IDropTarget dropTarget )
    {
      if( dropTarget == null )
        return;

      dropTarget.DragEnter( m_dragContext );
    }

    private void InvokeDragLeave( IDropTarget dropTarget )
    {
      if( dropTarget == null )
        return;

      dropTarget.DragLeave( m_dragContext );
    }

    private void InvokeDragOver( IDropTarget dropTarget, Point mousePosition )
    {
      if( dropTarget == null )
        return;

      dropTarget.DragOver( m_dragContext, mousePosition );
    }

    private void InvokeDragDrop( IDropTarget dropTarget )
    {
      if( dropTarget == null )
        return;

      dropTarget.Drop( m_dragContext );
    }

    private void BeginDrag()
    {
      if( m_isDragActive )
        return;

      DragDropCancelEventArgs eventArgs = new DragDropCancelEventArgs( m_dragContext );

      m_dragContext.OnDragStarting( eventArgs );

      if( eventArgs.Cancel )
        return;

      //Freeze the DragDropContext to ensure that any "data" that must not be changed after the beginning of the operation, stays the same.
      m_dragContext.FreezeDragContext();

      //Try to capture the mouse, if it fails, throw... It can only fail if:
      //  * The mouse is NOT over the Silverlight plug-in content area.
      //  * Another Silverlight object has already captured the mouse.
      //  * Another non-Silverlight object has already captured the mouse.
      //  * The left mouse button is NOT the pressed state.
      FrameworkElement draggedElement = m_dragContext.DraggedElement;

      m_isDragActive = true;
      m_previousCursor = draggedElement.Cursor;

      if( !draggedElement.CaptureMouse() )
      {
        m_isDragActive = false;
        m_previousCursor = null;
        return; //no drag to be started if capture is not possible.
      }

      DragDropAdornerManager.PrepareAdorner( m_dragContext, m_dragContext.LastMouseMoveEventArgs );

      draggedElement.Cursor = Cursors.Hand;

      m_dragContext.OnDragStarted();
    }

    private void EndDrag()
    {
      if( m_isDragActive )
      {
        if( this.DropOnTarget() )
        {
          if( m_isDragActive )
          {
            m_dragContext.OnDragCompleted( new DragDropEventArgs( m_dragContext ) );
          }
        }
        else
        {
          if( m_isDragActive )
          {
            m_dragContext.OnDragAborted( new DragDropEventArgs( m_dragContext ) );
          }
        }
      }

      this.CleanDrag();
    }

    private bool DropOnTarget()
    {
      bool canDrop = false;

      if( !m_isDragActive || m_draggedOverReference == null )
        return canDrop;

      //Drop the element on the current target if it is possible
      IDropTarget dropTarget = m_draggedOverReference.Target as IDropTarget;
      if( dropTarget == null )
        return canDrop;

      canDrop = dropTarget.CanDropElement( m_dragContext );

      if( canDrop )
      {
        this.InvokeDragDrop( dropTarget );
      }

      return canDrop;
    }

    private void CleanDrag()
    {
      if( m_dragContext != null )
      {
        FrameworkElement draggedElement = m_dragContext.DraggedElement;
        draggedElement.MouseMove -= OnDraggedElementMouseMove;
        draggedElement.MouseLeftButtonUp -= OnDraggedElementMouseLeftButtonUp;
        draggedElement.MouseEnter -= OnDraggedElementMouseEnter;
        draggedElement.MouseLeave -= OnDraggedElementMouseLeave;
        draggedElement.LostMouseCapture -= OnDraggedElementLostMouseCapture;

        //Release the mouse capture only if a BeginDrag has set the mouse capture earlier.
        if( m_isDragActive )
        {
          draggedElement.ReleaseMouseCapture();
        }

        draggedElement.Cursor = m_previousCursor;
      }

      DragDropAdornerManager.HideAdorner();

      m_previousCursor = null;
      m_customCursorPopup.IsOpen = false;

      m_draggedOverReference = null;

      m_isDragActive = false;
      m_dragContext = null;


      m_lastMousePosition = new Point();
      m_mouseLeftButtonDownLocation = new Point();

      ContentPresenter presenter = m_customCursorPopup.Child as ContentPresenter;
      presenter.ContentTemplate = null;
    }

    private void CancelDragCore()
    {
      if( m_isDragActive )
      {
        m_dragContext.OnDragAborted( new DragDropEventArgs( m_dragContext ) );
      }

      this.CleanDrag();
    }

    private void PauseDragCore()
    {
      if( m_isDragActive )
      {
        m_dragContext.OnDragPaused( new DragDropEventArgs( m_dragContext ) );
      }
    }

    private void ResumeDragCore()
    {
      if( m_isDragActive )
      {
        m_dragContext.OnDragResumed( new DragDropEventArgs( m_dragContext ) );
      }
    }

    private void UpdateCursor( Point mouseLocation,
      DragDropCursor cursor,
      DataTemplate customCursorTemplate )
    {
      bool isCustomCursor = false;

      FrameworkElement draggedElement = m_dragContext.DraggedElement as FrameworkElement;

      if( draggedElement == null )
        return;

      //Display the given cursor
      switch( cursor )
      {
        case DragDropCursor.Arrow:
          draggedElement.Cursor = Cursors.Arrow;
          break;

        case DragDropCursor.Hand:
          draggedElement.Cursor = Cursors.Hand;
          break;

        case DragDropCursor.IBeam:
          draggedElement.Cursor = Cursors.IBeam;
          break;

        case DragDropCursor.None:
          draggedElement.Cursor = Cursors.None;
          break;

        case DragDropCursor.SizeNS:
          draggedElement.Cursor = Cursors.SizeNS;
          break;

        case DragDropCursor.SizeWE:
          draggedElement.Cursor = Cursors.SizeWE;
          break;

        case DragDropCursor.Wait:
          draggedElement.Cursor = Cursors.Wait;
          break;

#if SILVERLIGHT
        case DragDropCursor.Eraser:
          draggedElement.Cursor = Cursors.Eraser;
          break;

        case DragDropCursor.No:
          //Workaround to simulate the "No" cursor
          draggedElement.Cursor = Cursors.None;
          isCustomCursor = true;

          if( customCursorTemplate == null )
            customCursorTemplate = m_noDropCursorTemplate;

          break;

        case DragDropCursor.Stylus:
          draggedElement.Cursor = Cursors.Stylus;
          break;
#else
        case DragDropCursor.Help:
          draggedElement.Cursor = Cursors.Help;
          break;

        case DragDropCursor.No:
          draggedElement.Cursor = Cursors.No;
          break;
#endif
        case DragDropCursor.Custom:
          draggedElement.Cursor = Cursors.None;
          isCustomCursor = true;
          break;

        default:
          draggedElement.Cursor = Cursors.Arrow;
          break;
      }

      // Use a custom cursor (hack) to display the custom cursor
      // from the DataTemplate
      m_customCursorPopup.IsOpen = isCustomCursor;

      if( isCustomCursor )
      {
        ContentPresenter contentPresenter = m_customCursorPopup.Child as ContentPresenter;

        if( contentPresenter != null )
          contentPresenter.ContentTemplate = customCursorTemplate;

        m_customCursorPopup.VerticalOffset = mouseLocation.Y;
        m_customCursorPopup.HorizontalOffset = mouseLocation.X;
      }
    }

    private void OnDraggedElementMouseMove( object sender, MouseEventArgs e )
    {
      if( m_dragContext != null )
      {
        m_dragContext.LastMouseMoveEventArgs = e;
      }

      //check if the minimum threshold is attain to start dragging the element
      if( !m_isDragActive )
      {
        Point mouseLocation = e.GetPosition( null );
        if( DragDropHelper.IsMouseMoveDrag( m_mouseLeftButtonDownLocation, mouseLocation ) )
        {
          this.BeginDrag();
        }
      }

      if( m_isDragActive )
      {
        this.PerformDragOperationHitTest( e );
        DragDropAdornerManager.OnAdornedElementMouseMove( e );
      }
    }

    private void OnDraggedElementMouseLeftButtonUp( object sender, MouseButtonEventArgs e )
    {
      if( m_isDragActive )
      {
        this.EndDrag();
      }
      else
      {
        this.CleanDrag();
      }
    }

    private void OnDraggedElementMouseEnter( object sender, MouseEventArgs e )
    {
      if( !m_isDragActive )
      {
        this.CleanDrag();
      }
    }

    private void OnDraggedElementMouseLeave( object sender, MouseEventArgs e )
    {
      if( !m_isDragActive )
      {
        this.CleanDrag();
      }
    }

    private void OnDraggedElementLostMouseCapture( object sender, MouseEventArgs e )
    {
      if( !m_isDragActive )
        return;

      //From here, we know that a ReleaseMouseCapture has been called on the dragged element
      //outside of the Drag & Drop manager.  In that case, we must abort the whole operation.
      m_dragContext.OnDragAborted( new DragDropEventArgs( m_dragContext ) );
      this.CleanDrag();
    }

    private void PerformDragOperationHitTest( MouseEventArgs e )
    {
      if( !m_isDragActive )
        return;

      Func<UIElement, Point> getRelativeLocation = element => e.GetPosition( element );
      bool canDrop = false;

      //Cleanup previous dragged over reference if there was no hit
      if( !this.HitTestDropTargets( getRelativeLocation, out canDrop ) )
      {
        if( m_draggedOverReference != null )
        {
          //Issue a DragLeave for the previously dragged over element
          this.InvokeDragLeave( m_draggedOverReference.Target as IDropTarget );

          m_draggedOverReference = null;
        }
      }

      //Notify the "owner" of the drag operation that the mouse has moved
#if SILVERLIGHT
      Point mouseLocation = e.GetPosition( null );
#else
      FrameworkElement source = e.Source as FrameworkElement;
      Point mouseLocation = e.GetPosition( source );
      mouseLocation = source.PointToScreen( mouseLocation );
#endif

      // We need to reverse the X axis in case the dragged element's flow direction
      // is RightToLeft. We do not support having different flow direction inside a grid.

#if !WINDOWS_PHONE
      Point mouseRelativeLocation = ( m_dragContext.DraggedElement.FlowDirection == FlowDirection.LeftToRight )
        ? new Point( mouseLocation.X - m_lastMousePosition.X, mouseLocation.Y - m_lastMousePosition.Y )
        : new Point( m_lastMousePosition.X - mouseLocation.X, mouseLocation.Y - m_lastMousePosition.Y );
#else
      Point mouseRelativeLocation = new Point( mouseLocation.X - m_lastMousePosition.X, mouseLocation.Y - m_lastMousePosition.Y );
#endif

      DragDropCursor cursor = m_dragContext.GetCursor( canDrop );

      DataTemplate customCursorTemplate = null;

      DragDropMoveEventArgs dragDropEventArgs = new DragDropMoveEventArgs( m_dragContext, mouseRelativeLocation, cursor );

      m_dragContext.OnDragMove( dragDropEventArgs );

      if( dragDropEventArgs.Cursor != cursor )
      {
        cursor = dragDropEventArgs.Cursor;
      }

      if( cursor == DragDropCursor.Custom )
        customCursorTemplate = m_dragContext.GetCustomCursorTemplate( canDrop );

      this.UpdateCursor( mouseLocation, cursor, customCursorTemplate );

      m_lastMousePosition = mouseLocation;
    }

    private bool HitTestDropTargets( Func<UIElement,Point> getRelativeLocation, out bool canDrop )
    {
      canDrop = false;
      bool hasHit = false;

      if( getRelativeLocation == null )
        return hasHit;


#if SILVERLIGHT
      IDropTarget dropTarget =
        System.Windows.Media.VisualTreeHelper.FindElementsInHostCoordinates(
        getRelativeLocation(null),
        Application.Current.RootVisual ).FirstOrDefault((elem) => elem is IDropTarget) as IDropTarget;
#else
      IDropTarget dropTarget = null;

      foreach( Window win in Application.Current.Windows.Cast<Window>().Reverse() )
      {
        // Use callback to find the templated parent
        System.Windows.Media.HitTestResultCallback callback = ( result ) =>
        {
          if( result.VisualHit is FrameworkElement )
          {
            dropTarget = ( ( FrameworkElement )result.VisualHit ).TemplatedParent as IDropTarget;
          }
          return ( dropTarget != null )
            ? System.Windows.Media.HitTestResultBehavior.Stop
            : System.Windows.Media.HitTestResultBehavior.Continue;
        };

        System.Windows.Media.VisualTreeHelper.HitTest(
          win,
          null,
          callback,
          new System.Windows.Media.PointHitTestParameters( getRelativeLocation( win ) ) );

        if( dropTarget != null )
          break;

        // Didn't found any dropTarget, look for VisualTree.GetParent
        HitTestResult parentResult = VisualTreeHelper.HitTest( win, getRelativeLocation( win ) );
        if( parentResult == null )
          continue;

        DependencyObject visual = parentResult.VisualHit;
        while( ( visual != null ) && !( visual is IDropTarget ) )
        {
          visual = VisualTreeHelper.GetParent( visual );
        }

        if( visual != null )
        {
          dropTarget = visual as IDropTarget;
          break;
        }
      }

#endif

      if( dropTarget == null )
        return false;

      bool allowDrop = dropTarget.CanDropElement( m_dragContext );
      canDrop |= allowDrop;

      //If the hit test returns an element other than the previously "dragged over" element
      if( dropTarget != DragDropManager.DraggedOverReference )
      {
        if( m_draggedOverReference != null )
        {
          //Issue a DragLeave for the previously dragged over element
          this.InvokeDragLeave( m_draggedOverReference.Target as IDropTarget );
        }

        m_draggedOverReference = new WeakReference(dropTarget);

        if( allowDrop )
        {
          //Issue a DragEnter for the current target
          this.InvokeDragEnter( dropTarget );
        }
      }

      if( allowDrop )
      {
        //Issue the drag over message to drop target
        this.InvokeDragOver( dropTarget, getRelativeLocation.Invoke( dropTarget as UIElement ) );
      }

      return true;
    }

    private Point m_mouseLeftButtonDownLocation;
    private Point m_lastMousePosition;

    private Popup m_customCursorPopup = new Popup();
    private DataTemplate m_noDropCursorTemplate; // = null;
    private Cursor m_previousCursor; // null
  }
}
